Phase 3:驗證閉環。這是整套流程的成敗關鍵。
今天講三支腳本。它們全部很短、很土,而且是那個 36/37 能成立的原因。
先看全貌:
| 層級 | 用什麼 | 驗什麼 | 成本 | 結果 |
|---|---|---|---|---|
| L1 | shell 腳本 | 檔案位置、命名、禁用 API、引用完整性 | 毫秒 | 確定 |
| L2 | 測試指令 | 後置條件與錯誤情境都有測試且通過 | 秒~分 | 確定 |
| L3 | 審查清單 | 業務邏輯、規格符合度 | token | 機率性 |
分層原則一句話:**能用便宜的方式擋掉的,就不要留到昂貴的那一層。**如果一個檔案放錯目錄,你不需要動用 AI 去審查它。一支 grep 就抓到了。而且:
確定性的問題,要用確定性的機制解決。
用 AI 去審查「這個檔案在不在正確的目錄」,等於把一個確定的答案換成一個大部分時候正確的答案。你付了更多錢,買到更差的保證。
check-structure.sh — 依照昨天講的編碼規範,檢查三件事:
第三項就是昨天講的「能被 grep 的規則」兌現的地方。舉個薪資系統的實際例子。有一條規則是:**保費一律走級距表,禁止用核定薪資直接乘費率。**這條規則在第 0 層的寫法是:
⚠️ 計算保費時**必須**先查級距表,**不可以**直接用薪資乘費率!
在第 3 層的寫法是一條 grep,抓「薪資變數 × 費率常數」的 pattern,出現就擋:
#!/bin/bash
# check-structure.sh —— 最小可跑版本
fail=0
# 1. 禁用 pattern:只看保費相關變數乘上費率變數,不是所有乘法
if grep -rqnE '(insuredSalary|核定薪資)\s*\*\s*(rate|費率|Rate)' src/ 2>/dev/null; then
grep -rnE '(insuredSalary|核定薪資)\s*\*\s*(rate|費率|Rate)' src/ >&2
echo "❌ 核薪直乘費率,保費必須走級距表" >&2; fail=1
fi
# 2. 分層歸屬:Repository 只能出現在 infra 層
if grep -rql 'class .*Repository' src/ 2>/dev/null | grep -qv '^src/infrastructure/'; then
echo "❌ Repository 出現在 infrastructure 之外" >&2; fail=1
fi
# 3. 命名慣例:Service 類別的檔名必須以 Service 結尾(不含 Tests/Helper/Factory)
for f in $(find src -name '*Service.*' -type f 2>/dev/null); do
case "$(basename "$f")" in *Service.*) ;; *) echo "❌ 命名不符:$f" >&2; fail=1 ;; esac
done
exit $fail
三十行不到,而且它永遠不會忘記。不會因為 context 太長而失焦、不會因為這次很趕而被跳過。
這三條的收窄很重要,而我第一版沒收。 我原本寫的是 (salary|核薪)[A-Za-z]*\s*\*\s*[0-9.]+。結果 salaryCount * 12 這種無關的乘法會命中;命名那條原本抓 *Service*,於是 ServiceTests、ServiceHelper 全被判命名不符。在一個完全合規的專案上跑,它噴三發紅字。
而下面兩節我自己寫著「腳本寫完之後一定要在現況下跑一次,確認它們是綠的。一旦紅字變成常態,這層驗證就失效了」。我在同一篇的兩百字之內違反了自己的規定。
(grep -rq 那三條的 pattern 你要換成自己的。這支腳本的價值不在我寫的內容,在它把「規則」變成一個有退出碼的東西。)(這條規則後來真的擋下過東西。有一次規則寫成直接相乘,被抓出來之後補了一條反例測試釘住。)
check-docs-consistency.sh — 這支是防「文件漂移」的。Day 13 講過,對 AI 來說過期的規範不是失效,是誤導。它會照著一個已經不存在的結構去產碼,而且不會報錯。這支至少檢查三件事:
一、規範文件引用的每一個路徑都必須存在。
fail=0
while IFS= read -r p; do
[ -n "$p" ] && { [ -e "$p" ] || { echo "❌ 引用了不存在的路徑: $p" >&2; fail=1; }; }
done < <(grep -oE '`\.(ai|dev)/[^`]+`' CLAUDE.md | tr -d '`' | sort -u)
exit $fail
這就是 Day 17 第三步那一支,同一份東西在這裡的職責是守第 1 層。兩個細節不要改掉。一個是用 while ... done < <(...),不要用 grep | while——管線右側是子 shell,exit 出不去。另一個是不要用 for p in $(grep ...),它會依空白斷字。這兩個坑 Day 17 都實測過。
還有,一定要有退出碼。只 echo 的版本擋不了任何東西,它只是在印字。
擋掉的是我認為最陰險的一類失效。AI 讀到不存在的路徑不會報錯,它會用腦中「編碼規範通常長什麼樣」的先驗繼續產碼。沒有錯誤訊息、沒有警告。你只會覺得「這次 AI 表現比較差」。
二、配置的單一真相與實際建置檔一致。
宣稱用哪個版本?實際的建置檔寫的是什麼?
Day 14 講過「單一真相自己會過期」。這一項就是在守那件事。第 1 層需要第 3 層來確保自己不腐化。
三、所有規格都通過 schema 驗證。
明天會講規格格式,這裡先記著:規格本身也是要被驗證的東西。
check-spec-coverage.sh — 這支是我原本以為最難寫的,後來發現它最有價值。它做的事:**對照規格裡的後置條件與錯誤情境,跟測試檔裡的斷言,列出缺漏。**為什麼有價值?因為它讓「規格覆蓋率」變成一個可以計算的數字,而不是一種感覺。
規格覆蓋率 = 有對應斷言的條目 ÷ 全部條目
最小可跑的版本大概是這樣。前提是規格用固定格式寫(明天講那個格式):
#!/bin/bash
# check-spec-coverage.sh <spec.json> <test-file>
spec="$1"; test="$2"
total=0; hit=0
# 規格裡的每一條後置條件與錯誤情境,都要在測試檔裡找得到對應的斷言 id
ids=$(jq -er '(.postconditions[]?, .errorCases[]?) | .id' "$spec") || {
echo "❌ 規格解析失敗(格式不符 schema?)" >&2; exit 2; }
[ -n "$ids" ] || { echo "❌ 規格裡一條後置條件與錯誤情境都沒有" >&2; exit 2; }
for id in $ids; do
total=$((total+1))
grep -q "$id" "$test" && hit=$((hit+1)) || echo " ❌ 未覆蓋:$id" >&2
done
echo "覆蓋率 $hit/$total"
[ "$hit" -eq "$total" ] # 退出碼:全覆蓋 0,有缺漏 1
那三行 fail-closed 不是裝飾。 我第一版沒寫,結果是:規格格式不符的時候 jq 會噴錯、迴圈一圈都不跑、total 和 hit 都是 0,最後 0 -eq 0 回傳 PASS。
一個永遠回報通過的覆蓋率檢查,比沒有檢查更危險。而那正好是後面會講到的「假象一:0 條被當成全綠」。我在自己的腳本上重現了它。
關鍵是那個 id。 規格的每一條要有編號,測試的每一個斷言要在註解或測試名稱裡帶上那個編號。這樣「對帳」才是機器做得到的事,而不是人讀兩份檔案憑印象比對。(這支只驗「有沒有對應的斷言」,不驗那個斷言寫得對不對。它抓不到「有,但斷言是錯的」。那一層要靠 review。)
而那 43 次執行的紀錄裡,你會看到這樣的東西:
覆蓋率 12/12 + 20/20 + 10/10
覆蓋率 13/13 + 13/13
覆蓋率 9/9
每一次執行都有這個數字。 它不是「大概都測到了」,是「規格有 12 條,測試對到 12 條」。實務上有兩種做法:
我建議從寬鬆版開始,等命名穩定了再往嚴格版收。一開始就要求標註,團隊會覺得太繁瑣而抗拒。

我看過的導入案例裡,L1 是最常被略過的一層。理由通常是:
第三個最危險,因為它聽起來最合理。
但回想 Day 20 那條硬規定:驗證必須先於自動化。先自動化再補驗證,中間那段時間你在用一條沒有品管的產線高速產出東西。
而且還有一個更根本的理由:
**L1 是唯一一層可以在「AI 剛寫完的那一秒」就攔截的驗證。**L2 要跑完整套測試,L3 要花 token 和時間。只有 L1 便宜到可以掛在每一次檔案寫入之後。也就是 Day 16 講的 hook。
越早攔截,修正成本越低。 這是很老的軟體工程常識,只是在 AI 產碼的節奏下被放大了。因為產出速度快到你來不及用人工 review 去接。
腳本寫完之後一定要在現況下跑一次,確認它們是綠的。
如果一寫完就有一堆紅字,會發生兩件事的其中一件:要嘛你花三天去修既有的違規(可能值得、可能不值得),要嘛。更常見的——大家開始習慣忽略紅字。
而一旦紅字變成常態,這層驗證就失效了。
這跟 Day 16 講的那個 --quiet 設計是同一個道理:警訊必須稀有,才有意義。
所以如果既有程式碼違規太多,正確做法是先把檢查範圍限定在「新增的檔案」,等存量慢慢清完再擴大。
用 Day 19 那個判準看這三支腳本,分工會突然變得很清楚:
跑這三支腳本是工站——它做錯了,退出碼會告訴你,下游抓得到,所以可以完全交給機器,而且應該每次都跑。
決定這三支腳本要檢查什麼是閘門——它漏了一項,不會有任何機制告訴你「你漏了」。
所以這裡真正不能外包的不是執行,是那份檢查清單。腳本可以請 AI 寫,但「該檢查哪些東西」這件事沒有下游。(這一條後面會再應驗一次,而且是應驗在我自己寫的腳本上。)
明天講規格格式——以及一個關鍵的設計決定:規格要描述什麼,才能讓上面那個「覆蓋率」算得出來。